import {
  Environment,
  FormDataKeyValue,
  HoppRESTAuth,
  HoppRESTHeader,
  HoppRESTHeaders,
  HoppRESTParam,
  HoppRESTParams,
  HoppRESTReqBody,
  HoppRESTRequest,
  parseBodyEnvVariables,
  parseRawKeyValueEntriesE,
  parseTemplateString,
  parseTemplateStringE,
} from "@hoppscotch/data"
import * as A from "fp-ts/Array"
import * as E from "fp-ts/Either"
import { flow, pipe } from "fp-ts/function"
import * as O from "fp-ts/Option"
import * as RA from "fp-ts/ReadonlyArray"
import * as S from "fp-ts/string"
import qs from "qs"
import { combineLatest, Observable } from "rxjs"
import { map } from "rxjs/operators"

import { generateAuthHeaders, generateAuthParams } from "../auth/auth-types"
import { stripComments } from "../editor/linting/jsonc"
import { arrayFlatMap, arraySort } from "../functional/array"
import { toFormData } from "../functional/formData"
import { tupleWithSameKeysToRecord } from "../functional/record"
import { isJSONContentType } from "./contenttypes"

export interface EffectiveHoppRESTRequest extends HoppRESTRequest {
  /**
   * The effective final URL.
   *
   * This contains path, params and environment variables all applied to it
   */
  effectiveFinalURL: string
  effectiveFinalHeaders: HoppRESTHeaders
  effectiveFinalParams: HoppRESTParams
  effectiveFinalBody: FormData | string | null | File | Blob
  effectiveFinalRequestVariables: { key: string; value: string }[]
}

/**
 * Get headers that can be generated by authorization config of the request
 * @param req Request to check
 * @param envVars Currently active environment variables
 * @param auth Authorization config to check
 * @param parse Whether to parse the template strings
 * @param showKeyIfSecret Whether to show the key if the value is a secret
 * @returns The list of headers
 */
export const getComputedAuthHeaders = async (
  envVars: Environment["variables"],
  req?:
    | HoppRESTRequest
    | {
        auth: HoppRESTAuth
        headers: HoppRESTHeaders
      },
  auth?: HoppRESTRequest["auth"],
  showKeyIfSecret = false
) => {
  const request = auth ? { auth: auth ?? { authActive: false } } : req

  if (!request || !request.auth || !request.auth.authActive) return []

  return await generateAuthHeaders(
    request.auth,
    req as HoppRESTRequest,
    envVars,
    showKeyIfSecret
  )
}

/**
 * Get headers that can be generated by body config of the request
 * @param req Request to check
 * @returns The list of headers
 */
export const getComputedBodyHeaders = (
  req:
    | HoppRESTRequest
    | {
        auth: HoppRESTAuth
        headers: HoppRESTHeaders
      }
): HoppRESTHeader[] => {
  // If a content-type is already defined, that will override this
  if (
    req.headers.find(
      (req) => req.active && req.key.toLowerCase() === "content-type"
    )
  )
    return []

  if (!("body" in req)) return []

  // Body should have a non-null content-type
  if (!req.body || req.body.contentType === null) return []

  if (
    req.body &&
    req.body.contentType === "application/octet-stream" &&
    req.body.body
  ) {
    const filename = req.body.body.name
    const fileType = req.body.body.type

    const contentType = fileType ? fileType : "application/octet-stream"

    return [
      {
        active: true,
        key: "content-type",
        value: contentType,
        description: "",
      },
      {
        active: true,
        key: "Content-Disposition",
        value: `attachment; filename="${filename}"`,
        description: "",
      },
    ]
  }

  return [
    {
      active: true,
      key: "content-type",
      value: req.body.contentType,
      description: "",
    },
  ]
}

export type ComputedHeader = {
  source: "auth" | "body"
  header: HoppRESTHeader
}

/**
 * Returns a list of headers that will be added during execution of the request
 * For e.g, Authorization headers maybe added if an Auth Mode is defined on REST
 * @param req The request to check
 * @param envVars The environment variables active
 * @param parse Whether to parse the template strings
 * @param showKeyIfSecret Whether to show the key if the value is a secret
 * @returns The headers that are generated along with the source of that header
 */
export const getComputedHeaders = async (
  req:
    | HoppRESTRequest
    | {
        auth: HoppRESTAuth
        headers: HoppRESTHeaders
      },
  envVars: Environment["variables"],
  showKeyIfSecret = false
): Promise<ComputedHeader[]> => {
  return [
    ...(
      await getComputedAuthHeaders(envVars, req, undefined, showKeyIfSecret)
    ).map((header) => ({
      source: "auth" as const,
      header,
    })),
    ...getComputedBodyHeaders(req).map((header) => ({
      source: "body" as const,
      header,
    })),
  ]
}

export type ComputedParam = {
  source: "auth"
  param: HoppRESTParam
}

/**
 * Returns a list of params that will be added during execution of the request
 * For e.g, Authorization params (like API-key) maybe added if an Auth Mode is defined on REST
 * @param req The request to check
 * @param envVars The environment variables active
 * @returns The params that are generated along with the source of that header
 */
export const getComputedParams = async (
  req: HoppRESTRequest,
  envVars: Environment["variables"]
): Promise<ComputedParam[]> => {
  if (!req.auth || !req.auth.authActive) return []

  const params = await generateAuthParams(req.auth, req, envVars)
  return params.map((param) => ({ source: "auth" as const, param }))
}

// Resolves environment variables in the body
export const resolvesEnvsInBody = (
  body: HoppRESTReqBody,
  env: Environment
): HoppRESTReqBody => {
  if (!body.contentType) return body

  if (body.contentType === "application/octet-stream") {
    return body
  }

  if (body.contentType === "multipart/form-data") {
    if (!body.body) {
      return {
        contentType: null,
        body: null,
      }
    }

    return {
      contentType: "multipart/form-data",
      body: body.body.map(
        (entry) =>
          <FormDataKeyValue>{
            active: entry.active,
            isFile: entry.isFile,
            key: parseTemplateString(entry.key, env.variables, false, true),
            value: entry.isFile
              ? entry.value
              : parseTemplateString(entry.value, env.variables, false, true),
            contentType: entry.contentType,
          }
      ),
    }
  }

  let bodyContent = ""

  if (isJSONContentType(body.contentType))
    bodyContent = stripComments(body.body)

  if (body.contentType === "application/x-www-form-urlencoded") {
    bodyContent = body.body
  }

  return {
    contentType: body.contentType,
    body: parseTemplateString(bodyContent, env.variables, false, true),
  }
}

export function getFinalBodyFromRequest(
  request: HoppRESTRequest,
  envVariables: Environment["variables"],
  showKeyIfSecret = false
): FormData | Blob | string | null {
  if (request.body.contentType === null) return null

  if (request.body.contentType === "application/x-www-form-urlencoded") {
    const parsedBodyRecord = pipe(
      request.body.body ?? "",
      parseRawKeyValueEntriesE,
      E.map(
        flow(
          RA.toArray,
          /**
           * Filtering out empty keys and non-active pairs.
           */
          A.filter(({ active, key }) => active && !S.isEmpty(key)),

          /**
           * Mapping each key-value to template-string-parser with either on array,
           * which will be resolved in further steps.
           */
          A.map(({ key, value }) => [
            parseTemplateStringE(key, envVariables, false, showKeyIfSecret),
            parseTemplateStringE(value, envVariables, false, showKeyIfSecret),
          ]),

          /**
           * Filtering and mapping only right-eithers for each key-value as [string, string].
           */
          A.filterMap(([key, value]) =>
            E.isRight(key) && E.isRight(value)
              ? O.some([key.right, value.right] as [string, string])
              : O.none
          ),
          tupleWithSameKeysToRecord,
          (obj) => qs.stringify(obj, { indices: false })
        )
      )
    )
    return E.isRight(parsedBodyRecord) ? parsedBodyRecord.right : null
  }

  if (request.body.contentType === "multipart/form-data") {
    return pipe(
      request.body.body ?? [],
      A.filter(
        (x) =>
          x.key !== "" &&
          x.active &&
          (typeof x.value === "string" ||
            (x.value.length > 0 && x.value[0] instanceof File))
      ), // Remove empty keys and unsetted file

      // Sort files down
      arraySort((a, b) => {
        if (a.isFile) return 1
        if (b.isFile) return -1
        return 0
      }),

      // FormData allows only a single blob in an entry,
      // we split array blobs into separate entries (FormData will then join them together during exec)
      arrayFlatMap((x) =>
        x.isFile
          ? (Array.isArray(x.value) ? x.value : [x.value]).map((v) => ({
              key: parseTemplateString(x.key, envVariables),
              value: v as string | Blob,
              contentType: x.contentType,
            }))
          : [
              {
                key: parseTemplateString(x.key, envVariables),
                value: parseTemplateString(x.value, envVariables),
                contentType: x.contentType,
              },
            ]
      ),
      toFormData
    )
  }

  if (request.body.contentType === "application/octet-stream") {
    return request.body.body
  }

  let bodyContent = request.body.body ?? ""

  if (isJSONContentType(request.body.contentType))
    bodyContent = stripComments(request.body.body)

  // body can be null if the content-type is not set
  return parseBodyEnvVariables(bodyContent, envVariables)
}

/**
 * Outputs an executable request format with environment variables applied
 *
 * @param request The request to source from
 * @param environment The environment to apply
 * @param showKeyIfSecret Whether to show the key if the value is a secret
 *
 * @returns An object with extra fields defining a complete request
 */
export async function getEffectiveRESTRequest(
  request: HoppRESTRequest,
  environment: Environment,
  showKeyIfSecret = false
): Promise<EffectiveHoppRESTRequest> {
  const effectiveFinalHeaders = pipe(
    (
      await getComputedHeaders(request, environment.variables, showKeyIfSecret)
    ).map((h) => h.header),
    A.concat(request.headers),
    A.filter((x) => x.active && x.key !== ""),
    A.map((x) => ({
      active: true,
      key: parseTemplateString(
        x.key,
        environment.variables,
        false,
        showKeyIfSecret
      ),
      value: parseTemplateString(
        x.value,
        environment.variables,
        false,
        showKeyIfSecret
      ),
      description: x.description,
    }))
  )

  const effectiveFinalParams = pipe(
    (await getComputedParams(request, environment.variables)).map(
      (p) => p.param
    ),
    A.concat(request.params),
    A.filter((x) => x.active && x.key !== ""),
    A.map((x) => ({
      active: true,
      key: parseTemplateString(
        x.key,
        environment.variables,
        false,
        showKeyIfSecret
      ),
      value: parseTemplateString(
        x.value,
        environment.variables,
        false,
        showKeyIfSecret
      ),
      description: x.description,
    }))
  )

  const effectiveFinalRequestVariables = pipe(
    request.requestVariables,
    A.filter((x) => x.active && x.key !== ""),
    A.map((x) => ({
      active: true,
      key: parseTemplateString(x.key, environment.variables),
      value: parseTemplateString(x.value, environment.variables),
    }))
  )

  const effectiveFinalBody = getFinalBodyFromRequest(
    request,
    environment.variables,
    showKeyIfSecret
  )

  return {
    ...request,
    effectiveFinalURL: parseTemplateString(
      request.endpoint,
      environment.variables,
      false,
      showKeyIfSecret
    ),
    effectiveFinalHeaders,
    effectiveFinalParams,
    effectiveFinalBody,
    effectiveFinalRequestVariables,
  }
}

/**
 * Creates an Observable Stream that emits HoppRESTRequests whenever
 * the input streams emit a value
 *
 * @param request$ The request stream containing request data
 * @param environment$ The environment stream containing environment data to apply
 *
 * @returns Observable Stream for the Effective Request Object
 */
export function getEffectiveRESTRequestStream(
  request$: Observable<HoppRESTRequest>,
  environment$: Observable<Environment>
): Observable<Promise<EffectiveHoppRESTRequest>> {
  return combineLatest([request$, environment$]).pipe(
    map(async ([request, env]) => await getEffectiveRESTRequest(request, env))
  )
}
